<html>
  <head>
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>
  <body>
  <script src="three.js"></script>
  <script>
    let container, scene, camera, session, model;
    let mesher = null;

    const localVector = new THREE.Vector3();
    const localVector2 = new THREE.Vector3();
    const localEuler = new THREE.Euler();
    const localMatrix = new THREE.Matrix4();
    const localFloat32Array = new Float32Array(16);

    const numCubes = 100;
    const cubeSize = 0.2;
    const cubeRange = 1;
    const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);

    function init() {
      container = document.createElement('div');
      document.body.appendChild(container);

      scene = new THREE.Scene();
      scene.matrixAutoUpdate = false;
      // scene.background = new THREE.Color(0x3B3961);

      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
      // camera.position.set(0, 1, 0);
      camera.lookAt(new THREE.Vector3());
      scene.add(camera);

      const ambientLight = new THREE.AmbientLight(0x808080);
      scene.add(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
      directionalLight.position.set(1, 1, 1);
      scene.add(directionalLight);

      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);

      // window.browser.magicleap.RequestDepthPopulation(true);
      // renderer.autoClear = false;

      container.appendChild(renderer.domElement);

      const terrainMeshes = [];
      const terrainMaterial = new THREE.MeshPhongMaterial({
        color: 0x666666,
        polygonOffset: true,
        polygonOffsetFactor: -1,
        polygonOffsetUnits: -1,
      });
      const _getTerrainMesh = meshId => {
        let terrainMesh = terrainMeshes.find(terrainMesh => terrainMesh.meshId === meshId);
        if (!terrainMesh) {
          terrainMesh = _makeTerrainMesh(meshId);
          terrainMeshes.push(terrainMesh);
          scene.add(terrainMesh);
        }
        return terrainMesh;
      };
      const fakeArrayBuffer = new ArrayBuffer(3 * 4);
      const fakeFloat32Array = new Float32Array(fakeArrayBuffer, 0, 3);
      const fakeUint16Array = new Uint16Array(fakeArrayBuffer, 0, 3);
      const _makeTerrainMesh = meshId => {
        const geometry = new THREE.BufferGeometry();
        const gl = renderer.getContext();
        const attributes = renderer.getAttributes();

        geometry.addAttribute('position', new THREE.BufferAttribute(fakeFloat32Array, 3));
        attributes.update(geometry.attributes.position, gl.ARRAY_BUFFER);
        geometry.addAttribute('normal', new THREE.BufferAttribute(fakeFloat32Array, 3));
        attributes.update(geometry.attributes.normal, gl.ARRAY_BUFFER);
        geometry.setIndex(new THREE.BufferAttribute(fakeUint16Array, 1));
        attributes.update(geometry.index, gl.ELEMENT_ARRAY_BUFFER);

        const material = terrainMaterial;

        const mesh = new THREE.Mesh(geometry, material);
        mesh.matrixAutoUpdate = false;
        mesh.frustumCulled = false;
        mesh.meshId = meshId;
        return mesh;
      };
      const _loadTerrainMesh = (terrainMesh, {transformMatrix, positionBuffer, positionCount, normalBuffer, normalCount, indexBuffer, count}) => {
        terrainMesh.matrix.fromArray(transformMatrix);
        terrainMesh.matrixWorldNeedsUpdate = true;

        const {geometry} = terrainMesh;
        const attributes = renderer.getAttributes();

        attributes.get(geometry.attributes.position).buffer = positionBuffer;
        geometry.attributes.position.count = positionCount / 3;

        attributes.get(geometry.attributes.normal).buffer = normalBuffer;
        geometry.attributes.normal.count = normalCount / 3;

        attributes.get(geometry.index).buffer = indexBuffer;
        geometry.index.count = count / 1;
      };
      const _removeTerrainMesh = terrainMesh => {
        scene.remove(terrainMesh);
        terrainMesh.geometry.dispose();
      };
      const _clearTerrainMeshes = () => {
        for (let i = 0; i < terrainMeshes.length; i++) {
          _removeTerrainMesh(terrainMeshes[i]);
        }
        terrainMeshes.length = 0;
      };
      const _onMesh = updates => {
        for (let i = 0; i < updates.length; i++) {
          const update = updates[i];
          const {id, type} = update;

          if (type === 'new' || type === 'update') {
            _loadTerrainMesh(_getTerrainMesh(id), update);
          } else if (type === 'unchanged') {
            // nothing
          } else {
            const index = terrainMeshes.findIndex(terrainMesh => terrainMesh.meshId === id);
            if (index !== -1) {
              const terrainMesh = terrainMeshes[index];
              _removeTerrainMesh(terrainMesh);
              terrainMeshes.splice(index, 1);
            }
          }
        }
      };

      if (window.browser && window.browser.magicleap) {
        mesher = window.browser.magicleap.RequestMeshing();
        mesher.onmesh = _onMesh;
      } else {
        const gl = renderer.getContext();

        const geometryPositions = cubeGeometry.attributes.position.array;
        const geometryNormals = cubeGeometry.attributes.normal.array;
        const geometryIndices = cubeGeometry.index.array;

        /* for (let j = 0; j < geometryPositions.length; j += 3) {
          localVector
            .fromArray(geometryPositions, j)
            .multiplyScalar(cubeSize)
            .toArray(geometryPositions, j);
        } */

        const positionArray = new Float32Array(new ArrayBuffer(numCubes * geometryPositions.length * Float32Array.BYTES_PER_ELEMENT));
        const normalArray = new Float32Array(new ArrayBuffer(numCubes * geometryNormals.length * Float32Array.BYTES_PER_ELEMENT));
        const indexArray = new Uint16Array(new ArrayBuffer(numCubes * geometryIndices.length * Float32Array.BYTES_PER_ELEMENT));
        for (let i = 0; i < numCubes; i++) {
          const positionDstOffset = i*geometryPositions.length;
          const offsetVector = localVector2.set((Math.random()-0.5)*2*cubeRange, (Math.random()-0.5), (Math.random()-0.5)*2*cubeRange);
          const offsetEuler = localEuler.set((Math.random()-0.5)*2*Math.PI, (Math.random()-0.5)*2*Math.PI, (Math.random()-0.5)*2*Math.PI, 'YXZ');
          for (let j = 0; j < geometryPositions.length; j += 3) {
            localVector
              .fromArray(geometryPositions, j)
              .multiplyScalar(cubeSize)
              .applyEuler(offsetEuler)
              .add(offsetVector)
              .toArray(positionArray, positionDstOffset + j);
          }

          const normalDstOffset = i*geometryNormals.length;
          for (let j = 0; j < geometryNormals.length; j++) {
            normalArray[normalDstOffset + j] = geometryNormals[j];
          }

          const indexDstOffset = i*geometryIndices.length;
          const indexSrcOffset = i*geometryPositions.length/3;
          for (let j = 0; j < geometryIndices.length; j++) {
            indexArray[indexDstOffset + j] = geometryIndices[j] + indexSrcOffset;
          }
        }

        const transformMatrix = localMatrix
          .fromArray(window.document.xrOffset.matrix)
          .getInverse(localMatrix)
          .toArray(localFloat32Array);

        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, positionArray, gl.STATIC_DRAW);
        const positionCount = positionArray.length;

        const normalBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, normalArray, gl.STATIC_DRAW);
        const normalCount = normalArray.length;

        const indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW);
        const count = indexArray.length;

        renderer.state.reset();

        const updates = [
          {
            id: 0,
            type: 'update',
            transformMatrix,
            positionArray,
            positionBuffer,
            positionCount,
            normalArray,
            normalBuffer,
            normalCount,
            indexArray,
            indexBuffer,
            count,
          },
        ];
        setInterval(() => {
          _onMesh(updates);
        }, 100);
      }

      renderer.setAnimationLoop(animate);
    }

    function animate(time, frame) {
      if (model) {
        const animationTime = 4000;
        const f = ((Date.now() % animationTime) / animationTime) * (Math.PI * 2);
        model.quaternion.setFromUnitVectors(
          new THREE.Vector3(0, 0, 1),
          new THREE.Vector3(Math.cos(f), 0, Math.sin(f)).normalize()
        );
        model.updateMatrixWorld();
      }

      renderer.render(scene, renderer.vr.enabled ? renderer.vr.getCamera(camera) : camera);
    }

    init();

    (async () => {
      console.log('request session');
      session = await navigator.xr.requestSession({
        exclusive: true,
      }).catch(err => Promise.resolve(null));

      if (session) {
        // console.log('request first frame');
        session.requestAnimationFrame((timestamp, frame) => {
          renderer.vr.setSession(session, {
            frameOfReferenceType: 'stage',
          });

          const {views} = frame.getViewerPose();
          const viewport = session.baseLayer.getViewport(views[0]);
          const width = viewport.width;
          const height = viewport.height;

          renderer.setSize(width * 2, height);

          renderer.setAnimationLoop(null);

          renderer.vr.enabled = true;
          renderer.vr.setAnimationLoop(animate);

          console.log('running!');
        });
      } else {
        console.log('no xr devices');
      }
    })();
  </script>
  </body>
</html>
